package mnstudent.security;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.security.AlgorithmParameters;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.InvalidParameterSpecException;
import java.security.spec.KeySpec;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;
/**
 *
 * @author Administrator
 */
public class AES {

    private static final int ITERATIONS = 65536;

    private static final String STRING_ENCODING = "UTF-8";

    private static final String CRYPT_PREFIX = "CRYPT";

    /**
     * If we user Key size of 256 we will get java.security.InvalidKeyException:
     * Illegal key size or default parameters , Unless we configure Java
     * Cryptography Extension 128
     */
    private static final int KEY_SIZE = 128;

    //SALT String to byte
    //Chuỗi Salt (Mật Khẩu) 
    private static final byte[] SALT = {0x7b, 0x45, 0x43, 0x31, 0x41, 0x32, 0x38, 0x30, 0x31, 0x2d, 0x31, 0x33, 0x32, 0x32, 0x2d, 0x34, 0x34, 0x45, 0x39, 0x2d, 0x39, 0x45, 0x36, 0x32, 0x2d, 0x36, 0x39, 0x39, 0x38, 0x35, 0x32, 0x36, 0x36, 0x33, 0x42, 0x41, 0x35, 0x7d};

    private SecretKeySpec secret;

    private Cipher cipher;
            
    public AES() throws EncryptDecryptException {
        try {
            /* Derive the key, given password and salt. */
            //Create Key
            SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
            KeySpec spec;

            spec = new PBEKeySpec(getHardwareKey(), SALT, ITERATIONS, KEY_SIZE);
            SecretKey tmp = factory.generateSecret(spec);
            secret = new SecretKeySpec(tmp.getEncoded(), "AES");

            // CBC = Cipher Block chaining
            // PKCS5Padding Indicates that the keys are padded
            cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");

        } catch (EncryptDecryptException | NoSuchAlgorithmException | InvalidKeySpecException | NoSuchPaddingException e) {
            throw new EncryptDecryptException("Unable to initialize", e);
        }

    }

    /**
     * Get hardware key to be used for encryption, we rely on MAC address as the
     * key, with fallback to JVM name then windows machine name
     *
     * @return key as char[]
     * @throws EncryptDecryptException
     */
    private char[] getHardwareKey() throws EncryptDecryptException {

        // Exceptions from this are ignored as we move to next available method
        try {
            InetAddress address = InetAddress.getLocalHost();
            NetworkInterface nic = NetworkInterface.getByInetAddress(address);
            if (nic != null) {
                byte[] mac = nic.getHardwareAddress();
                if (mac != null && mac.length > 0) {
                    return new String(mac, STRING_ENCODING).toCharArray();
                }
            }
        } catch (UnknownHostException | SocketException | UnsupportedEncodingException e) {
            e.printStackTrace();
        }

        // Could not obtain MAC Address so we are falling back on the computer
        // name then on the JVM Name
        RuntimeMXBean rmx = ManagementFactory.getRuntimeMXBean();
        String jvmName = rmx.getName();
        String[] parts = jvmName.split("@");
        if (parts.length > 0) {
            String name = parts[1];
            if (name != null && !name.isEmpty()) {
                return name.toCharArray();
            }
        }

        String name = System.getenv("COMPUTERNAME");
        if (name != null && !name.isEmpty()) {
            return name.toCharArray();
        }
        throw new EncryptDecryptException("Unable to obtain Secure Key");
    }

    /**
     * Encrypt given input string
     *
     * @param input
     * @return
     * @throws EncryptDecryptException
     */
    public String encrypt(String input) throws EncryptDecryptException {
        try {
            byte[] inputBytes = input.getBytes(STRING_ENCODING);
            cipher.init(Cipher.ENCRYPT_MODE, secret);
            AlgorithmParameters params = cipher.getParameters();
            byte[] iv = params.getParameterSpec(IvParameterSpec.class).getIV();
            byte[] ciphertext = cipher.doFinal(inputBytes);
            byte[] out = new byte[iv.length + ciphertext.length];
            System.arraycopy(iv, 0, out, 0, iv.length);
            System.arraycopy(ciphertext, 0, out, iv.length, ciphertext.length);
            return CRYPT_PREFIX + ":" + Base64.encodeBase64String(out);
        } catch (IllegalBlockSizeException | BadPaddingException | InvalidKeyException | InvalidParameterSpecException | UnsupportedEncodingException e) {
            throw new EncryptDecryptException("Unable to encrypt", e);
        }
    }

    /**
     * Decrypt input string
     *
     * @param input
     * @return decrypted string
     * @throws EncryptDecryptException
     */
    @SuppressWarnings("restriction")
    public String decrypt(String input) throws EncryptDecryptException {
        if (!input.startsWith(CRYPT_PREFIX)) {
            throw new EncryptDecryptException("Unable to decrypt, input string does not start with 'CRYPT'");
        }

        try {
            byte[] data = Base64.decodeBase64(input.substring(6, input.length()));
            //byte[] data = base64Decoder.decodeBuffer(input.substring(6, input.length()));
            int keylen = KEY_SIZE / 8;
            byte[] iv = new byte[keylen];
            System.arraycopy(data, 0, iv, 0, keylen);
            cipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(iv));
            return new String(cipher.doFinal(data, keylen, data.length - keylen), STRING_ENCODING);
        } catch (IOException | InvalidAlgorithmParameterException | InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) {
            throw new EncryptDecryptException("Unable to decrypt ", e);
        }
    }

    /**
     * Helper Exception to wrap arround Encryption/Decryption exceptions
     */
    @SuppressWarnings("serial")
    public static class EncryptDecryptException extends Exception {

        public EncryptDecryptException() {
            super();
        }

        public EncryptDecryptException(String msg) {
            super(msg);
        }

        public EncryptDecryptException(String msg, Throwable t) {
            super(msg, t);
        }

        public EncryptDecryptException(Throwable t) {
            super(t);
        }
    }

    /**
     * Check if given string is already encrypted
     *
     * @param input
     * @return
     */
    public boolean isEncrypted(String input) {
        if (input == null || input.isEmpty()) {
            return false;
        }
        return input.startsWith(CRYPT_PREFIX);
    }
}
